﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using System.Xml.Serialization;

namespace Amf4Asmx.Web
{
    public class WebTypeData : WebObjectData
    {
        private delegate void SetValueDelegate(object target, object value);
        private delegate object GetValueDelegate(object target);
        private List<SetValueDelegate> orderedProperties = new List<SetValueDelegate>();
        private Dictionary<string, SetValueDelegate> namedSetProperties = new Dictionary<string, SetValueDelegate>();
        private Dictionary<string, GetValueDelegate> namedGetProperties = new Dictionary<string, GetValueDelegate>();
        private Dictionary<string, object> defaultValues = new Dictionary<string, object>();
        private Type type;

        public WebTypeData(Type t)
        {
            this.type = t;
            MemberInfo[] members = GetFieldsAndProperties(t.GetMembers(BindingFlags.Instance | BindingFlags.Public));
            foreach (MemberInfo mi in members)
            {
                string name = mi.Name; // TODO - is there a different name we should use?

                object[] attributes = mi.GetCustomAttributes(typeof(DefaultValueAttribute), true);
                if (attributes.Length > 0)
                {
                    defaultValues[name] = (attributes[0] as DefaultValueAttribute).Value;
                }
                
                
                SetValueDelegate setDelegate;
                GetValueDelegate getDelegate;
                if (mi is FieldInfo)
                {
                    FieldInfo fi = mi as FieldInfo;
                    setDelegate = (target, value) => fi.SetValue(target, TypeHelper.Instantiate(fi.FieldType, value));
                    getDelegate = fi.GetValue;
                }
                else if (mi is PropertyInfo)
                {
                    PropertyInfo pi = mi as PropertyInfo;
                    setDelegate = (target, value) => pi.GetSetMethod().Invoke(target, new object[] { TypeHelper.Instantiate(pi.PropertyType, value) });
                    getDelegate = (target) => pi.GetGetMethod().Invoke(target, null);
                }
                else
                {
                    continue;
                }

                if (mi.GetCustomAttributes(typeof(XmlIgnoreAttribute), true).Length > 0)
                {
                    continue;
                }

                orderedProperties.Add(setDelegate);
                namedSetProperties.Add(name, setDelegate);
                namedGetProperties.Add(name, getDelegate);
            }
        }

        public static MemberInfo[] GetFieldsAndProperties(MemberInfo[] memberInfo)
        {
            List<MemberInfo> members = new List<MemberInfo>();
            foreach (MemberInfo m in memberInfo)
            {
                if (m is FieldInfo || m is PropertyInfo)
                {
                    members.Add(m);
                }
            }
            return members.ToArray();
        }

        public override object Instantiate(object properties)
        {
            object result = Activator.CreateInstance(type);
            if (properties is AmfObjects.NamedDictionary)
            {
                properties = (properties as AmfObjects.NamedDictionary).Properties;
            }

            if (properties is IDictionary<string, object>)
            {
                foreach (KeyValuePair<string, object> kvp in (properties as IDictionary<string, object>))
                {
                    if (this.namedSetProperties.ContainsKey(kvp.Key))
                    {
                        try
                        {
                            this.namedSetProperties[kvp.Key](result, kvp.Value);
                        }
                        catch
                        {
                            throw new InvalidOperationException(string.Format("Unable to instantiate object type '{0}'. Could not set property '{1}', given value '{2}'.", type.Name, kvp.Key, (kvp.Value ?? "null").ToString()));
                        }
                    }
                    else
                    {
                        StringBuilder sb = new StringBuilder();
                        foreach (string key in namedSetProperties.Keys)
                        {
                            if (sb.Length > 0)
                            {
                                sb.Append(", ");
                            }
                            sb.Append("'").Append(key).Append("'");
                        }
                        StringBuilder sb2 = new StringBuilder("Given properties ");
                        if (properties is IDictionary<string, object>)
                        {
                            foreach (string key in (properties as IDictionary<string, object>).Keys)
                            {
                                if (sb2.Length > 0)
                                {
                                    sb2.Append(", ");
                                }
                                sb2.Append("'").Append(key).Append("'");
                            }
                        }
                        sb2.Append(".");
                        throw new InvalidOperationException(string.Format("Unable to instantiate object type '{0}'. Could not find property '{3}'.  Has properties {1}.  {2}", type.Name, sb.ToString(), sb2.ToString(), kvp.Key));
                    }
                }
            }
            else
            {
                throw new NotImplementedException();
            }
            return result;
        }


        public AmfObjects.NamedDictionary GetNamedDictionary(object p)
        {
            AmfObjects.NamedDictionary result = new AmfObjects.NamedDictionary();
            result.TypeName = type.Name;

            foreach (KeyValuePair<string, GetValueDelegate> kvp in namedGetProperties)
            {
                object value = kvp.Value(p);

                // ignore default values
                try
                {
                    if (value is IComparable && (value as IComparable).CompareTo(defaultValues[kvp.Key]) == 0)
                    {
                        continue;
                    }
                }
                catch { }

                // ignore self-referencing values
                try
                {
                    if (value is IComparable && (value as IComparable).CompareTo(p) == 0)
                    {
                        continue;
                    }
                }
                catch { }
                result.Properties[kvp.Key] = value;
            }

            return result;
        }
    }
}
